Popular Searches
Popular Course Categories
Popular Courses

Implementing the Runnable Interface

Implementing the Runnable Interface

Multithreading and Concurrency Fundamentals

Implementing the Runnable Interface in Java

Course: Core Java Training

Topic: Implementing the Runnable Interface

Course Link: Core Java Training Course

Demo Link: Register for a Course Demo

JustAcademy Website: Visit JustAcademy


1. Introduction to the Runnable Interface

Java supports multithreading, which allows multiple threads to make progress concurrently within a program. One common way to define a task for a thread is to implement the Runnable interface.

The Runnable interface belongs to the java.lang package and represents a task that can be executed by a thread. It defines a single abstract method named run(). A class that implements Runnable provides the code that should execute when the task runs.

Implementing Runnable separates the task's logic from the thread that executes it. Instead of extending the Thread class, a class can implement Runnable and pass its object to a Thread instance. This approach is especially useful when a class already extends another class or when the same task needs to be submitted to different execution mechanisms.

2. Learning Objectives

  • Understand the purpose of the Runnable interface.
  • Learn how to implement and override the run() method.
  • Create and start a thread using a Runnable object.
  • Understand the difference between start() and run().
  • Compare implementing Runnable with extending Thread.
  • Use anonymous classes, lambda expressions, and method references with Runnable.
  • Understand thread synchronization, interruption, and common implementation mistakes.
  • Apply the Runnable interface in practical Java programs.

3. What Is the Runnable Interface?

Runnable is a functional interface used to represent an operation that does not return a result. Its main method is:

public interface Runnable {
    void run();
}

The interface is annotated with @FunctionalInterface in the Java API, which means it has one abstract method and can be implemented using a lambda expression or method reference.

When a Runnable object is supplied to a Thread and that thread is started, the task's run() method is invoked in the new thread of execution.

Important Characteristics

  • Package: java.lang
  • Main method: run()
  • Return type: void
  • Parameters: None
  • Purpose: Defines a task to execute.
  • Thread creation: A Runnable object alone does not start a new thread.
  • Implementation: Can be implemented by a class, anonymous class, or lambda expression.

4. Why Use the Runnable Interface?

Java allows a class to extend only one class. If a class extends Thread, it cannot extend another class at the same time. Implementing Runnable avoids this limitation because a class can implement interfaces while extending a class.

Another important benefit is separation of concerns. The task contains the business logic, while the Thread object manages the execution thread. The task can also be reused with executor services and thread pools.

Advantages of Runnable

  • Separates task logic from thread management.
  • Allows the class to extend another class.
  • Makes task objects easier to reuse.
  • Supports lambda expressions and method references.
  • Works with thread pools and executor services.
  • Encourages a flexible design for concurrent applications.

5. Steps to Implement the Runnable Interface

Follow these steps to create and execute a thread using Runnable:

  1. Create a class that implements Runnable.
  2. Override the run() method with the task's logic.
  3. Create an object of the class.
  4. Pass the object to the Thread constructor.
  5. Call start() on the thread object.
  6. Allow the JVM scheduler to execute the task.

6. Basic Example of Implementing Runnable

The following program demonstrates a simple task that prints a message from a separate thread.

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Task is running in a separate thread");
    }
}

public class Main {
    public static void main(String[] args) {
        MyTask task = new MyTask();

        Thread thread = new Thread(task);
        thread.start();
    }
}

Output:

Task is running in a separate thread

Explanation

  • MyTask implements the Runnable interface.
  • The run() method contains the task to execute.
  • new MyTask() creates the task object.
  • new Thread(task) creates a thread configured to execute the task.
  • thread.start() starts the new thread, which then invokes the task's run() method.

7. Example: Runnable with a Loop

A Runnable task can contain loops, calculations, method calls, or other application logic just like an ordinary Java method.

class NumberPrinter implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("Number: " + i);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        NumberPrinter task = new NumberPrinter();
        Thread thread = new Thread(task);

        thread.start();
    }
}

Output:

Number: 1
Number: 2
Number: 3
Number: 4
Number: 5

This output is typical for this single task, but when several threads print simultaneously, their output can interleave in different orders depending on scheduling.

8. Running Multiple Threads with Runnable

The same Runnable implementation can be used to create multiple thread objects. Each thread independently executes the task, although shared objects or variables used by those tasks may require synchronization.

class Worker implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(
                Thread.currentThread().getName() + " - " + i
            );
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Worker task = new Worker();

        Thread first = new Thread(task, "Worker-A");
        Thread second = new Thread(task, "Worker-B");

        first.start();
        second.start();
    }
}

Possible output:

Worker-A - 1
Worker-B - 1
Worker-A - 2
Worker-B - 2
Worker-B - 3
Worker-A - 3

The actual order may differ. Both threads execute the same task object. This is safe in this example because the task does not maintain mutable instance state; if a shared task modifies common data, appropriate thread-safety measures may be necessary.

9. Difference Between start() and run()

Understanding the difference between start() and run() is essential when working with the Runnable interface.

Feature start() run()
Purpose Starts a new thread of execution. Contains or invokes the task's execution logic.
Thread behavior Schedules execution on a separate thread. A direct method call executes on the calling thread.
JVM behavior The new thread invokes its target's run() method. Does not itself create a new thread.
Typical use Call it on a newly created Thread. Override it in a task or call it indirectly through a thread.

Example

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println(
            "Executing on: " + Thread.currentThread().getName()
        );
    }
}

public class Main {
    public static void main(String[] args) {
        MyTask task = new MyTask();

        task.run();

        Thread thread = new Thread(task, "Background-Worker");
        thread.start();
    }
}

Possible output:

Executing on: main
Executing on: Background-Worker

The direct call to task.run() runs on the main thread. The call to thread.start() runs the task on a separate thread.

10. Implementing Runnable with a Lambda Expression

Because Runnable is a functional interface, Java allows a lambda expression to provide the implementation of its single abstract method. This approach is concise and common in modern Java applications.

public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            System.out.println("Runnable using a lambda expression");
        };

        Thread thread = new Thread(task);
        thread.start();
    }
}

Output:

Runnable using a lambda expression

The lambda expression acts as the implementation of run(). It does not create or start a thread by itself; the Thread object and its start() method provide the separate execution.

11. Implementing Runnable with an Anonymous Class

An anonymous class can implement Runnable without declaring a separate named class. This style is useful when the task is small and only needed in one location.

public class Main {
    public static void main(String[] args) {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println("Task from anonymous class");
            }
        };

        new Thread(task).start();
    }
}

Output:

Task from anonymous class

For short tasks, a lambda expression is usually more concise. Anonymous classes remain useful when the implementation requires additional methods or more complex class behavior.

12. Implementing Runnable with a Method Reference

A method reference can be used when an existing method matches the signature required by Runnable. For example, a method that takes no arguments and returns void can be referenced as a task.

public class Main {
    static void printMessage() {
        System.out.println("Executing an existing method");
    }

    public static void main(String[] args) {
        Runnable task = Main::printMessage;
        Thread thread = new Thread(task);

        thread.start();
    }
}

Output:

Executing an existing method

The method reference Main::printMessage supplies the implementation for run(). The method executes on the thread that runs the task.

13. Runnable Versus Extending the Thread Class

Java offers two common approaches for defining thread tasks: implementing Runnable and extending Thread. They are related, but they represent different design choices.

Feature Implementing Runnable Extending Thread
Relationship Defines a task that can be executed by a thread. Defines a specialized thread class.
Inheritance The class can extend another class. The class already extends Thread.
Task reuse The same task can be passed to multiple thread objects or execution frameworks. The task is coupled to the thread subclass.
Thread management Separate from the task implementation. Combined with the subclass.
Lambda support Yes, because Runnable is a functional interface. A subclass of Thread is not itself a functional interface.
Recommended use Usually preferred when defining task logic. Useful when genuinely customizing thread behavior.

14. Using Runnable with Thread.sleep()

The sleep() method pauses the currently executing thread for a specified time. It is often used in demonstrations, timed tasks, and simple simulations. It does not release monitor locks that the current thread holds.

class TimerTaskExample implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println("Count: " + i);

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println("Task interrupted");
                return;
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new TimerTaskExample());
        thread.start();
    }
}

Output:

Count: 1
Count: 2
Count: 3

The values are printed approximately one second apart. The sleep duration is not a guarantee of exact scheduling time. The example also restores the interrupt status and exits the task if an interruption occurs.

15. Exception Handling in Runnable

The run() method does not declare checked exceptions in its method signature. Therefore, checked exceptions raised inside the task must be caught and handled within the implementation or converted into an appropriate unchecked exception.

class FileProcessingTask implements Runnable {
    @Override
    public void run() {
        try {
            processFile();
        } catch (java.io.IOException e) {
            System.err.println("File processing failed: " + e.getMessage());
        }
    }

    private void processFile() throws java.io.IOException {
        System.out.println("Processing file...");
        // File-processing logic would be placed here.
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new FileProcessingTask());
        thread.start();
    }
}

Output:

Processing file...

In this example, processFile() declares that it may throw IOException, so the task handles that checked exception inside run(). In production applications, exception handling should also record useful diagnostic information and communicate failures to the appropriate application layer.

16. Thread Safety When Using Runnable

Implementing Runnable does not automatically make a task thread-safe. If multiple threads access and modify the same mutable object, race conditions may occur unless access is coordinated correctly.

Example: Synchronized Counter

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

class CounterTask implements Runnable {
    private final Counter counter;

    CounterTask(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread first = new Thread(new CounterTask(counter));
        Thread second = new Thread(new CounterTask(counter));

        first.start();
        second.start();

        first.join();
        second.join();

        System.out.println(counter.getCount());
    }
}

Output:

2000

The two tasks share the same Counter object. Its synchronized methods protect the shared counter from lost updates, and the calls to join() ensure both threads complete before the final value is read.

17. Using Runnable with ExecutorService

In real applications, creating a new thread manually for every task may be inefficient. Java's executor framework allows tasks to be submitted to a thread pool, which manages thread creation and reuse.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Runnable task = () -> {
            System.out.println(
                "Task executed by " + Thread.currentThread().getName()
            );
        };

        executor.submit(task);
        executor.submit(task);

        executor.shutdown();
    }
}

Possible output:

Task executed by pool-1-thread-1
Task executed by pool-1-thread-2

The actual thread names and execution order may vary. The executor schedules submitted tasks using its available worker threads. Calling shutdown() stops accepting new tasks while allowing previously submitted tasks to complete.

Note: If an application needs to wait until all submitted tasks have completed, it can use awaitTermination(), futures, or another coordination mechanism.

18. Runnable and Callable: Key Differences

Runnable is designed for a task that does not return a result. If a task needs to return a value or declare a checked exception through its method signature, Java provides the Callable<V> interface.

Feature Runnable Callable
Method run() call()
Return value No return value; return type is void. Returns a value of type V.
Checked exceptions Cannot declare checked exceptions in run(). call() can declare checked exceptions.
Typical use Tasks such as logging, sending messages, or updating data. Tasks that calculate and return results.
Executor usage Can be submitted to an executor. Can be submitted to an executor to obtain a Future result.

19. Common Mistakes When Implementing Runnable

Mistake Why It Is a Problem Correct Approach
Calling run() directly The task executes on the current thread. Pass the task to a Thread and call start().
Assuming Runnable starts a thread automatically The interface only defines task behavior. Use a Thread or an executor service to run it.
Ignoring interruption Tasks may fail to stop when cancellation is requested. Handle InterruptedException and restore interrupt status when appropriate.
Sharing mutable data without protection Concurrent modifications can produce inconsistent results. Use synchronization, locks, atomic variables, or concurrent collections as appropriate.
Assuming a fixed output order Thread scheduling is nondeterministic. Use explicit coordination when ordering matters.
Creating too many threads Excessive threads consume resources and increase scheduling overhead. Consider a suitable executor or thread pool.
Swallowing exceptions silently Failures become difficult to detect and diagnose. Log, report, or otherwise handle errors meaningfully.

20. Best Practices for Runnable

  • Prefer implementing Runnable when defining a task rather than extending Thread without a specific reason.
  • Keep the run() method focused on the task's execution logic.
  • Use meaningful thread names when manually creating threads.
  • Use executor services for managing collections of tasks in many production applications.
  • Protect shared mutable state using suitable concurrency mechanisms.
  • Do not rely on thread scheduling to establish a particular execution order.
  • Handle interruption as a cooperative cancellation request.
  • Use Callable and Future when results or exception propagation are required.
  • Avoid deprecated thread-control methods such as stop(), suspend(), and resume().
  • Test concurrent code carefully because race conditions may not appear consistently.

21. Practice Exercises

  1. Create a class named GreetingTask that implements Runnable and prints a greeting message.
  2. Implement a Runnable class that prints numbers from 1 to 10.
  3. Create two threads using the same task object and observe how their output interleaves.
  4. Write a lambda expression that implements Runnable and prints the current thread name.
  5. Create a task that pauses using Thread.sleep() and handles interruption correctly.
  6. Build a shared counter example and protect its updates using synchronization.
  7. Submit several Runnable tasks to an ExecutorService and shut the executor down properly.
  8. Explain the difference between implementing Runnable and extending Thread.

22. Interview Questions and Answers

Q1. What is the Runnable interface in Java?

Answer: Runnable is a functional interface in java.lang that defines the run() method. It represents a task that can be executed by a thread and does not return a result.

Q2. Why is Runnable preferred over extending Thread in many cases?

Answer: It separates task logic from thread management and allows the implementing class to extend another class. It also supports task reuse with thread pools and executor services.

Q3. Does implementing Runnable create a new thread?

Answer: No. Implementing Runnable defines the task. A new thread can be created by passing the task to a Thread object and calling start(), or the task can be submitted to an executor.

Q4. What is the purpose of the run() method?

Answer: The run() method contains the code that should execute when the task is run. When a thread with a Runnable target is started, that thread invokes the target's run() method.

Q5. What happens if run() is called directly?

Answer: It behaves like an ordinary method call and executes on the calling thread. It does not start a new thread.

Q6. Can Runnable be implemented using a lambda expression?

Answer: Yes. Runnable is a functional interface with one abstract method, so a lambda expression can provide its implementation.

Q7. Can multiple threads use the same Runnable object?

Answer: Yes. Multiple thread objects can use the same Runnable instance. If the task shares or modifies mutable state, that state must be managed safely.

Q8. Can the run() method return a value?

Answer: No. Its return type is void. Use Callable<V> when a task needs to return a value.

Q9. How does Runnable work with ExecutorService?

Answer: An executor accepts Runnable tasks and schedules them for execution, often using a pool of reusable worker threads. This separates task submission from thread management.

Q10. Is Runnable automatically thread-safe?

Answer: No. Thread safety depends on the task implementation and the data it accesses. Shared mutable state may require synchronization or other concurrency controls.

23. Quick Revision Checklist

  • Runnable belongs to the java.lang package.
  • Its single abstract method is void run().
  • A Runnable object defines a task but does not start a thread by itself.
  • Pass the task to a Thread constructor and call start() to execute it on a new thread.
  • Calling run() directly does not create a new thread.
  • Runnable can be implemented using a normal class, anonymous class, lambda expression, or method reference.
  • Multiple threads can execute the same task object.
  • Shared mutable data must be protected when concurrent access can cause race conditions.
  • Use Callable when a task needs to return a result.
  • Consider ExecutorService for managing multiple tasks.

24. Conclusion

The Runnable interface is a fundamental part of Java multithreading. It provides a simple way to define a task independently from the thread that executes it. By implementing run() and passing the task to a Thread or an executor service, developers can build reusable and maintainable concurrent applications.

Remember that implementing Runnable does not automatically create a new thread or guarantee thread safety. Correct use of start(), appropriate coordination, interruption handling, and protection of shared data are essential for reliable multithreaded programming.

Continue learning with the Core Java Training Course, register for a course demo, or visit the JustAcademy website.

Previous Next
whatsapp